﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Net;
using System.Net.Mail;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Text.RegularExpressions;
using Db4objects.Db4o;
using Db4objects.Db4o.CS;
using Db4objects.Db4o.Linq;
using SharedObjects;
using SharedObjects.Registration;

namespace TheGameWebsite
{
	public class Registration : IRegistration
	{
		private IObjectContainer DB
		{
			get
			{
				try
				{
					return Db4oClientServer.OpenClient("localhost", 31336, "admin", "admin"); //this throws an exception if the server isn't answering. If that's the case, we open our own server.
				}
// ReSharper disable RedundantCatchClause
				catch (Exception)
				{
					//Todo: This event logging fails due to security reasons. investigate and fix. Disabled in the mean time.
#if USE_EVENTLOG
					bool sourceExists = false;
					sourceExists = EventLog.SourceExists("TheGame.Registration WCF Service");
					if (!sourceExists)
						EventLog.CreateEventSource("TheGame.Registration WCF Service", "System");
					EventLog log = new EventLog();
					log.Source = "TheGame.Registration WCF Service";
					log.WriteEntry("The registration service could not connect to the database. The exception was:\n" + ex.Message, EventLogEntryType.Error);
#endif
					throw;
				}
// ReSharper restore RedundantCatchClause
			}
		}

		private readonly Regex _emailRegex;
		private readonly Regex _agentNameRegex;
		public Registration()
		{
			_emailRegex = new Regex(@"\A(?<user>[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*)@(?<domain>[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)+)\Z", RegexOptions.Compiled);
			_agentNameRegex = new Regex(@"\A(?<name>\S{4,16})\Z", RegexOptions.Compiled);
		}

		public bool RequestEmailConfirmCode(string email)
		{
			IObjectContainer db;
			try
			{
				db = DB;
			}
			catch(Exception)
			{
				return false;
			}
#if DEBUG
			//var users = (from RegisteredUser ru in db select ru);
			//foreach (var usr in users)
			//    db.Delete(usr);
#endif
			var emailConfirms = (from EmailConfirmation ec in db where ec.CreationTimestamp < DateTime.Now.AddHours(-6) select ec);
			foreach (var ec in emailConfirms)
				db.Delete(ec);
			db.Commit();

			if (!_emailRegex.IsMatch(email))
				return false;

			if(
				(from RegisteredUser ru in db 
					where ru.Email == email && 
					ru.UserGameAccount != null && //only accounts
					!string.IsNullOrEmpty(ru.PaypalProfileId) && //that are
					!string.IsNullOrEmpty(ru.PaypalPayerId) //completed post payment are dupechecked
				 select ru).Count() > 0
				)
				return false;

			Guid confirmationId = Guid.NewGuid();
			EmailConfirmation confirm = new EmailConfirmation {CreationTimestamp = DateTime.Now, EmailAddress = email, Id = confirmationId.ToLong()};

			db.Store(confirm);
			db.Commit();

			SendConfirmationMailToUser(email, confirmationId);
			return true;
		}

		public ulong VerifyEmailAddressCode(string code)
		{
			ulong confirmationId;
			if (!ulong.TryParse(code, out confirmationId))
				return 0;
			IObjectContainer db = DB;
			EmailConfirmation ec = (from EmailConfirmation e in db where e.Id == confirmationId select e).FirstOrDefault();
			if (ec == null)
				return 0;
			RegisteredUser regUser = new RegisteredUser();
			regUser.Id = Guid.NewGuid().ToLong();
			regUser.Email = ec.EmailAddress;
			regUser.RegistrationDate = DateTime.Now;
			db.PersistEntity(regUser);
			db.Commit();
			return regUser.Id;
		}

		public bool CheckAgentName(string name)
		{
			name = name.Trim();
			if(!_agentNameRegex.IsMatch(name))
				return false;

			List<string> forbiddenNames = new List<string>
			{
				"root", "r00t", "test", "admin",
				"adm1n", "administrator", "4dm1n1str4t0r"
			};
			if (forbiddenNames.Contains(name.ToLower()))
				return false;

			IObjectContainer db = DB;
			var agent = (from Agent a in db where a.DisplayName.ToLower() == name.ToLower() select a).Count();
			if (agent > 0)
				return false;
			return true;
		}

		public List<Corporation> GetAvailableCorporations()
		{
			IObjectContainer db = DB;
			var list = (from Corporation c in db where c.DifficultyModifier > 4 && c.Name != "Administration" && (from ComputerSystem cs in db where cs.Corporation.Id == c.Id select cs).Count() >= 5 select c).ToList();
			return list;
		}

		public bool SaveAgentNameAndPassword(ulong regId, string name, string password, ulong corporationId)
		{
			IObjectContainer db = DB;
			RegisteredUser user = (from RegisteredUser ru in db where ru.Id == regId select ru).FirstOrDefault();
			if (user == null)
				return false;
			Corporation corp = (from Corporation c in db where c.Id == corporationId select c).FirstOrDefault();
			if (corp == null)
				return false;

			var corporationServers = (from ComputerSystem cs in db where cs.Corporation.Id == corp.Id select cs).ToList();
			if (corporationServers.Count < 5) //if incomplete corp was chosen
				return false;

			user.DesiredAgentName = name;
			user.DesiredPassword = password;
			user.DesiredCorporationId = corp.Id;
			db.PersistEntity(user);
			db.Commit();
			return true;
		}

		public string SubmitPaymentData(ulong regId, RegisteredUser userData)
		{
			IObjectContainer db = DB;
			RegisteredUser user = (from RegisteredUser ru in db where ru.Id == regId select ru).FirstOrDefault();
			if (user == null)
				return null;
			string token = "noToken";

#if USE_PAYPAL
			PaypalController ctrl = new PaypalController();
			if(!ctrl.CheckoutBeforeRedirect(regId, user.Email, out token))
				return null;

			user.PaypalToken = token;
			user.PaypalTransactionTimestamp = DateTime.Now;
			user.PaypalPayerId = null;

			db.PersistEntity(user);
			db.Commit();
#endif
			return token;
		}

		public bool PaypalConfirmation(ulong regId, string token, string payerId)
		{
			IObjectContainer db = DB;
			RegisteredUser user = (from RegisteredUser ru in db where ru.Id == regId select ru).FirstOrDefault();
			if (user == null)
				return false;

			if (user.PaypalToken != token)
				return false;
			
			user.PaypalPayerId = payerId;
			user.UserGameAccount = CompleteAgent(user);
			if (user.UserGameAccount == null)
			{
				SendErrorMailToUser(user);
				db.Rollback();
				return false;
			}

			string profileId = "noProfile";
#if USE_PAYPAL
			PaypalController ctrl = new PaypalController();
			profileId = ctrl.CreateRecurringProfile(token, user.Email);
			if (profileId == null)
			{
				SendErrorMailToUser(user);
				db.Rollback();
				return false;
			}
#endif
			user.UserGameAccount.AccountStatus = AccountStatus.Active;
			user.PaypalProfileId = profileId;
			db.PersistEntity(user);
			db.Commit();
			try
			{
			}
			catch
			{
				SendErrorMailToUser(user);
				db.Rollback();
				return false;
			}

			SendRegistrationSuccessMailToUser(user);
			return true;
		}

		public bool CancelTransaction(ulong regId, string token)
		{
			IObjectContainer db = DB;
			RegisteredUser user = (from RegisteredUser ru in db where ru.Id == regId && ru.PaypalToken == token select ru).FirstOrDefault();
			if (user == null) //no registered user found with that id/token combo.
				return false;

			if(user.PaypalPayerId != null) //payerId is in the db -> this is a successful transation and cannot be cancelled
				return false;

			user.PaypalToken = null;
			db.PersistEntity(user);
			db.Commit();
			return true;
		}

		private Account CompleteAgent(RegisteredUser user)
		{
			IObjectContainer db = DB;
			var existingUser = (from RegisteredUser ru in db where ru.Id == user.Id select ru).FirstOrDefault();
			if (existingUser == null)
				return null;

			Corporation corp = (from Corporation c in db where c.Id == existingUser.DesiredCorporationId select c).FirstOrDefault();
			if (corp == null)
				return null;

			Account newAccount = new Account();
			newAccount.Id = Guid.NewGuid().ToLong();
			newAccount.AccountStatus = AccountStatus.Incomplete;
			newAccount.Login = existingUser.Email;
			newAccount.Password = existingUser.DesiredPassword;
			newAccount.RegistrationDateReal = DateTime.Now;
			newAccount.RegistrationDateWorld = DateTime.Now;
			newAccount.Agent = new Agent { Id = Guid.NewGuid().ToLong(), DisplayName = existingUser.DesiredAgentName, Corporation = corp };
			existingUser.UserGameAccount = newAccount;
			
			Agent agent = newAccount.Agent;
			agent.Gateway = CreateFreshAgentGateway(ref db, agent.Corporation);

			#region Email accounts
			ComputerSystem emailServer = (from ComputerSystem cs in db where cs.Corporation.Id == agent.Corporation.Id && cs.IsDefaultEmailServer select cs).FirstOrDefault();
			if (emailServer == null)
				throw new Exception("No email server found for this corporation.");
			EmailAccount ea = new EmailAccount
			{
				Id = Guid.NewGuid().ToLong(),
				Name = agent.DisplayName,
				SystemId = emailServer.Id,
				Emails = new List<EmailHeader>()
			};
			UsenetAccount una = new UsenetAccount
			{
				AccountStatus = UsenetAccountStatus.Active,
				EmailAccountId = ea.Id,
				IsAdmin = false,
				Id = Guid.NewGuid().ToLong()
			};

			ComputerSystemAccount emailServerCsa = new ComputerSystemAccount {CreationTime = DateTime.Now, Id = Guid.NewGuid().ToLong(), Username = agent.DisplayName, Credentials = new List<Credential>()};
			foreach (SecuritySystem ss in emailServer.SecuritySystem)
			{
				Credential cred;
				if (ss.CreateCredentials(agent.DisplayName, existingUser.DesiredPassword, emailServer.GetHostname(), out cred))
				{
					SystemFile credFileCopy = cred.GetCredentialFileCopy(db);
					if (credFileCopy != null)
					{
						int usedspace = agent.Gateway.ComputerSystem.RootDirectory.GetUsedMemory(db);
						int total = agent.Gateway.GetTotalMemory();
						int free = total - usedspace;
						if (credFileCopy.Size > free)
							throw new Exception("Not enough space on agent gateway for credential file copy.");
						credFileCopy.ParentId = agent.Gateway.ComputerSystem.RootDirectory.Id;
						db.PersistEntity(credFileCopy);
						db.Commit();
					}
					emailServerCsa.Credentials.Add(cred);
				}
			}
			emailServer.Accounts.Add(emailServerCsa);
			agent.EmailAccounts = new List<EmailAccount> {ea};
			agent.UsenetAccount = una;
			#endregion

			#region Bank accounts
			ComputerSystem bankServer = (from ComputerSystem cs in db where cs.Corporation.Id == agent.Corporation.Id && cs.IsDefaultBankServer select cs).FirstOrDefault();
			if (bankServer == null)
				throw new Exception("No bank server found for this corporation.");
			BankAccount hqBank = (from BankAccount b in db where b.SystemId == bankServer.Id && b.IsSystem select b).FirstOrDefault();
			if (hqBank == null)
				throw new Exception("No system bank account for corporation '" + agent.Corporation.Name + "'s bank hq.");

			BankAccount ba = new BankAccount
			{
				AccountNumber = Utils.GetBankAccountNumber(db, bankServer.Id),
				CachedAmount = 0,
				Id = Guid.NewGuid().ToLong(),
				IsInvisible = false,
				IsSystem = false,
				SystemId = bankServer.Id
			};

			ComputerSystemAccount bankServerCsa = new ComputerSystemAccount {CreationTime = DateTime.Now, Id = Guid.NewGuid().ToLong(), Username = agent.DisplayName, Credentials = new List<Credential>()};
			foreach (SecuritySystem ss in bankServer.SecuritySystem)
			{
				Credential cred;
				if (ss.CreateCredentials(agent.DisplayName, existingUser.DesiredPassword, bankServer.GetHostname(), out cred))
				{
					SystemFile credFileCopy = cred.GetCredentialFileCopy(db);
					if (credFileCopy != null)
					{
						int usedspace = agent.Gateway.ComputerSystem.RootDirectory.GetUsedMemory(db);
						int total = agent.Gateway.GetTotalMemory();
						int free = total - usedspace;
						if (credFileCopy.Size > free)
							throw new Exception("Not enough space on agent gateway for credential file copy.");
						credFileCopy.ParentId = agent.Gateway.ComputerSystem.RootDirectory.Id;
						db.PersistEntity(credFileCopy);
						db.Commit();
					}
					bankServerCsa.Credentials.Add(cred);
				}
			}
			bankServer.Accounts.Add(bankServerCsa);

			agent.BankAccounts = new List<BankAccount> {ba};
			
			BankTransaction bt = new BankTransaction
			{
				Amount = 1500,
				Id = Guid.NewGuid().ToLong(),
				Purpose = "Starting capital",
				ReceivingAccountId = ba.Id,
				SendingAccountId = hqBank.Id,
				TransactionTimeStamp = DateTime.Now
			};
			db.PersistEntity(bt);
			#endregion

			agent.OwnedHardware = new List<IHardware>();
			agent.SavedConnections = new List<SavedConnection>();
			agent.UserPrograms = new List<UserProgram>();
			agent.ComputerSystemAccounts  = new List<ComputerSystemAccount>();
			agent.ComputerSystemIds = new List<ulong>((from ComputerSystem cs in db where (cs.Corporation.Id == agent.Corporation.Id && !cs.AlwaysInvisible) || cs.AlwaysVisible select cs.Id));
			
			db.PersistEntity(emailServer);
			db.PersistEntity(bankServer);
			db.Commit();

			//for debugging purposes
#pragma warning disable 168
			ComputerSystem emailServerTest = (from ComputerSystem cs in db where cs.Corporation.Id == agent.Corporation.Id && cs.IsDefaultEmailServer select cs).FirstOrDefault();
#pragma warning restore 168

			return newAccount;
		}

		private Gateway CreateFreshAgentGateway(ref IObjectContainer db, Corporation corp)
		{
			Gateway gw = new Gateway();
			GatewayBlueprint gwp = (from GatewayBlueprint bp in db where bp.IsDefault select bp).FirstOrDefault();
			if(gwp ==null)
				throw new Exception("CreateFreshAgentGateway: no default blueprint.");
			Cpu cpu = (from Cpu c in db where c.IsDefault select c).FirstOrDefault();
			if(cpu == null)
				throw new Exception("CreateFreshAgentGateway: no default cpu.");
			MemoryType mem = (from MemoryType m in db where m.IsDefault select m).FirstOrDefault();
			if(mem == null)
				throw new Exception("CreateFreshAgentGateway: no default mem.");

			NetworkAdapter nic = (from NetworkAdapter nwa in db where nwa.IsDefault select nwa).FirstOrDefault();
			if(nic == null)
				throw new Exception("CreateFreshAgentGateway: no default nic.");

			gw.InstalledCpus = new List<Cpu> {cpu};
			gw.InstalledMemory = new List<MemoryType> {mem};
			gw.InstalledNics = new List<NetworkAdapter> {nic};
			gw.GatewayBlueprint = gwp;
			gw.Id = Guid.NewGuid().ToLong();

			var corpAddresses = (from ComputerSystem cs in db where cs.Corporation.Id == corp.Id select cs.NetworkAddress);
			byte classC = corpAddresses.Max(it => it.ClassC);
			byte classD = corpAddresses.Max(it => it.ClassD);
			if (classD == 0xff)
				if (classC == 0xff)
					throw new Exception("Out of address space for corporation " + corp.Name);
				else
				{
					classC++;
					classD = 0x01;
				}
			else
				classD++;

			NetworkAddress addr = new NetworkAddress
			{
				Id = Guid.NewGuid().ToLong(),
				ClassA = corpAddresses.Max(it => it.ClassA),
				ClassB = corpAddresses.Max(it => it.ClassB),
				ClassC = classC,
				ClassD = classD
			};

			gw.ComputerSystem = new ComputerSystem
			{
				AlwaysInvisible = true,
				ComputerSystemFeatures = new List<ComputerSystemFeature>(),
				ComputerSystemLogs = new List<ComputerSystemLog>(),
				Corporation = corp,
				DifficultyLevel = 6,
				Id = Guid.NewGuid().ToLong(),
				IsGatewayCentral = false,
				MaintenanceInterval = 0,
				Name = "User gateway",
				NetworkAddress = addr,
				RootDirectory = new SystemDirectory { Id = Guid.NewGuid().ToLong(), Name = "User root"},
				SecuritySystem = new List<SecuritySystem>(),
				TrustedConnectionOnly = true
			};
			return gw;
		}

		private string GetRequestIp()
		{
			OperationContext context = OperationContext.Current;
			MessageProperties messageProperties = context.IncomingMessageProperties;
			RemoteEndpointMessageProperty endpointProperty = messageProperties[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty;
			return endpointProperty != null ? endpointProperty.Address : "Unknown";
		}

		private void SendMailToSubscriber(string targetEmail, string subject, string messageBody)
		{
			string smtphost = ConfigurationManager.AppSettings["SmtpHost"];
			int smtpport = int.Parse(ConfigurationManager.AppSettings["SmtpPort"]);
			string smtpuser = ConfigurationManager.AppSettings["SmtpUser"];
			string smtppass = ConfigurationManager.AppSettings["SmtpPass"];
			string smtpfrom = ConfigurationManager.AppSettings["SmtpFrom"];
			SmtpClient smtp = new SmtpClient(smtphost, smtpport);
			smtp.EnableSsl = true;
			NetworkCredential cred = new NetworkCredential(smtpuser, smtppass);
			smtp.UseDefaultCredentials = true;
			smtp.DeliveryMethod = SmtpDeliveryMethod.Network;
			smtp.Credentials = cred;
			MailMessage message = new MailMessage(smtpfrom, targetEmail, subject, messageBody);
			smtp.Send(message);
		}

		private void SendRegistrationSuccessMailToUser(RegisteredUser user)
		{
			#region body
			string body = @"Greetings.

Congratulations and welcome to TheGame. Your account has now been created successfully and payment has been confirmed by Paypal.
You can now log in to TheGame, using your email address and the password you chose during registration.

Should you uninstall or lose your client by accident, you can re-install the latest version on

http://localhost:81/Client.aspx



Kind regards & have a lot of fun,

TheGame administration team

Information:
This email is part of the registration process for TheGame. The email address was entered by a user from IP address {REQUEST_IP}.
If you feel that this email was sent in error, please forward this email to abuse@TheGameDomain.";

			body = body.Replace("{REQUEST_IP", GetRequestIp());
			SendMailToSubscriber(user.Email, "TheGame registration complete", body);
			#endregion
		}

		private void SendConfirmationMailToUser(string email, Guid confirmationId)
		{
			string body = @"Greetings.

This is an email confirmation message. Please copy the code below and paste it into the Installation Routine.

{CONFIRMATION_CODE}

If you closed the Installation Routine, you have to request another code.

Kind regards,

TheGame administration team

Information:
This email is part of the registration process for TheGame. The email address was entered by a user from IP address {REQUEST_IP}.
If you feel that this email was sent in error, please contact us at abuse@thegamegame.com.";
			body = body.Replace("{CONFIRMATION_CODE}", confirmationId.ToLong().ToString());
			body = body.Replace("{REQUEST_IP", GetRequestIp());

			SendMailToSubscriber(email, "TheGame: Email Address Confirmation", body);
		}

		private void SendErrorMailToUser(RegisteredUser user)
		{
			#region body
			string body = @"Greetings.

To our regret there appears to have been an error during your registration for TheGame. We have kept the progress you had in our database. 
Thus the process can be continued, after a support engineer looked into the issue.
Please contact technical support via techsupport@thega.me, and state your reference code:

{CONFIRMATION_CODE}

The technical support will then finalize your subscription.

Please note: No funds have been withdrawn from your Paypal account. However we advise you to double check your Paypal account that there are no active subscriptions for this registration.

Kind regards,

TheGame administration team

Information:
This email is part of the registration process for TheGame. The email address was entered by a user from IP address {REQUEST_IP}.
If you feel that this email was sent in error, please contact us at abuse@thegamegame.com.";
			body = body.Replace("{CONFIRMATION_CODE}", user.Id.ToString());
			body = body.Replace("{REQUEST_IP", GetRequestIp());
			SendMailToSubscriber(user.Email, "TheGame registration error", body);
			#endregion
		}
	}
}
